<!DOCTYPE html> <html lang=zh-CN style><!--
 Page saved with SingleFile 
 url: https://imququ.com/post/x-forwarded-for-header-in-http.html 
 saved date: Wed Feb 26 2020 23:18:19 GMT+0800 (China Standard Time)
--><meta charset=utf-8><meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1"><meta name=format-detection content="telephone=no"><meta http-equiv=X-UA-Compatible content="IE=edge"> <title>HTTP 请求头中的 X-Forwarded-For | JerryQu 的小站</title><meta name=theme-color content=#007b8b><meta name=mobile-web-app-capable content=yes><meta name=application-name content="JerryQu 的小站"><meta name=msapplication-starturl content=https://imququ.com><meta name=msapplication-navbutton-color content=#007b8b><meta name=apple-mobile-web-app-capable content=yes><meta name=apple-mobile-web-app-title content="JerryQu 的小站"><meta name=apple-mobile-web-app-status-bar-style content=black-translucent><link rel=manifest href=https://imququ.com/static/manifest.json><link rel=search type=application/opensearchdescription+xml href=https://imququ.com/opensearch.xml title="JerryQu 的小站"><link rel=alternate type=application/rss+xml title="RSS 2.0" href=https://imququ.com/rss.html><style>*{margin:0;padding:0}html,body{height:100%}body{background:#ddd;color:#666;font-size:14px;font-family:"-apple-system","Open Sans","HelveticaNeue-Light","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,sans-serif}h3{font-size:1.3em}a{text-decoration:none;color:#076dd0}article{border-bottom:1px solid #ddd;border-top:1px solid #fff;position:relative;padding:30px 0}.container{position:relative;min-height:100%;max-width:1600px}.left-col{width:270px;position:fixed;height:100%;background-color:#007b8b;background-image:url();background-size:cover}.mid-col{position:absolute;left:0;right:0;min-height:100%;margin-left:270px;background:#fff}article .meta{position:relative;float:right;color:#555;font-size:.9em;line-height:2;width:auto;text-align:right}article .entry-content,article .meta a{color:#444}article .entry-content{font-size:16px;line-height:1.8;word-wrap:break-word;font-family:Arial,'Hiragino Sans GB',冬青黑,'Microsoft YaHei',微软雅黑,SimSun,宋体,Helvetica,Tahoma,'Arial sans-serif';-webkit-font-smoothing:antialiased}article h1.title{color:#333;font-size:2em;font-weight:300;line-height:35px;margin-bottom:25px}article .entry-content p{margin-top:15px}.mid-col .mid-col-container{padding:0 70px 0 40px}article .meta .date,article .meta .comment{position:relative}article .entry-content a:hover{text-decoration:underline}#header{width:100%;line-height:30px;height:auto;border-bottom:0;padding:30px 0;margin-left:50px}#main-nav,#sub-nav{float:none;margin-top:15px}#main-nav{margin-left:0}#sub-nav{position:relative}#content{width:100%;margin:0 auto}#footer{line-height:2.2;text-align:center;width:auto;padding:15px 70px 15px 40px;border-top:1px solid #fff;font-size:.9em}#header a{color:#fff;text-shadow:0 1px #666;transition:color .3s}#header h1{font-weight:300;font-size:30px;float:none}#main-nav ul li{margin-left:0;display:block;position:relative}#header .subtitle{color:#fff}#sub-nav .social{margin-bottom:10px}#footer .beian{color:#666}#header a:hover{color:#ccc}#header .profilepic a{display:block;width:160px;height:160px;border-radius:50%;background-image:url();background-size:100% 100%;margin:15px 0 20px -10px}#sub-nav .social a{border-radius:50%;display:inline-block;vertical-align:middle;text-indent:-9999px;margin:0 6px 15px;width:28px;height:28px;background-size:20px 20px;background-position:center center;background-repeat:no-repeat;opacity:.75;transition:opacity .3s}#sub-nav .social a:hover{opacity:1}#sub-nav .social a:first-of-type{margin-left:0}#sub-nav .social a:last-of-type{margin-right:0}#sub-nav .social a.twitter{background-image:url();background-color:#55cff8;border:1px solid #55cff8}#sub-nav .social a.search{background-image:url();background-color:#afb6ca;border:1px solid #afb6ca}#sub-nav .social a.rss{background-image:url();background-color:#ef7522;border:1px solid #ef7522}#sub-nav .social a.twitter:hover{border:1px solid #24c1f6}#sub-nav .social a.search:hover{border:1px solid #909ab6}#sub-nav .social a.rss:hover{border:1px solid #cf5d0f}@media screen and (max-width:1024px){article{padding-bottom:15px}.left-col{width:210px}.mid-col{margin-left:210px}article h1.title,article .entry-content{margin-left:0}.mid-col .mid-col-container{padding:0 20px}#header{margin-left:30px}#footer{padding:15px 20px}}@media screen and (max-width:640px){article{padding:20px 0}.left-col{position:relative;width:100%;background-image:none}.mid-col{margin-left:0;float:none;width:100%}article .meta{margin-bottom:10px;position:static;width:auto}.mid-col .mid-col-container{padding:0 10px}.mid-col article .meta{float:none;overflow:hidden}article .meta .date,article .meta .comment{display:inline;margin-right:5px;padding-left:0}article .meta .date{margin-right:30px}#header{padding:20px 0;margin-left:0;text-align:center}#main-nav{margin-top:10px}#footer{padding:15px 10px}#main-nav ul li{display:inline;margin:0 10px;text-align:center}#header .profilepic a{position:absolute;top:12px;left:12px;width:56px;height:56px;margin:0}#sub-nav .social,#sub-nav .social a{margin-bottom:0}#sub-nav .social a{opacity:1}#content #toc-container,#content #toc{float:none}#content #toc{max-width:100%;margin:0}}article strong{font-weight:700}article code{font-family:Consolas,Monaco,'Andale Mono',monospace;font-size:80%;margin:0 2px;padding:4px 5px;background-color:#eee;vertical-align:middle;border-radius:5px}article pre{font-size:14px;line-height:1.6;color:#5d6a6a;padding:.6em;border-left:5px solid #ccc;background-color:#f8f8f8;word-break:break-word;word-wrap:break-word;white-space:pre-wrap;position:relative;overflow:hidden}article pre code{border-radius:0;background-color:transparent;border:0;padding:0;margin:0;font-size:100%;display:block;position:relative}.total_thread{line-height:1.6}.page-navi{width:100%;line-height:20px;position:relative;border-top:1px solid #fff;border-bottom:1px solid #ddd;padding:20px 0;overflow:hidden}article .copyright-info{font-size:14px}article .expired-tips{padding:5px 10px;font-size:15px;border:1px solid #e2e2e2;background-color:#ffffc0;border-left:5px solid #fff000;color:#333;overflow:hidden}article .post-info{font-size:14px}article .entry-content ul,article .entry-content ol,article .entry-content h3,article .entry-content pre{margin-top:15px}article .entry-content ul,article .entry-content ol{margin-left:25px}article pre b.name{font-family:"Consolas","Liberation Mono",Courier,monospace;position:absolute;line-height:1;top:10px;right:10px;font-size:60px;color:#eee;pointer-events:none}article .entry-content .date{color:#666;font-size:14px}#comments{border-top:1px solid #fff;border-bottom:1px solid #ddd;padding:20px 0;min-height:350px}#comments h1.title{font-size:25px;font-weight:300;line-height:35px;margin-bottom:10px}.page-navi .prev{float:left}.page-navi .next{float:right}.hljs-keyword{color:#859900}.hljs-number,.hljs-string{color:#2aa198}.hljs-title,.hljs-built_in{color:#268bd2}.hljs-variable{color:#b58900}#toc-container,#toc{float:right}#toc{min-width:120px;max-width:260px;padding:6px;margin:0 0 15px 20px;border:1px solid #e2e2e2;font-size:14px}#toc strong{border-bottom:1px solid #e2e2e2;display:block}#toc p{padding:0 4px;margin:0}#toc ul{margin:.5em .5em .5em 1.5em}</style> <link rel=amphtml href=https://imququ.com/amp/post/x-forwarded-for-header-in-http.html><meta name=description content="X-Forwarded-For 是什么？X-Real-Ip 又是什么？它们要怎么用，会不会引入安全隐患？Web 应用中是否可以伪造 IP？这一切的答案尽在本文。"><meta name=twitter:card content=summary><meta name=twitter:site content=@qgy18><meta name=twitter:title content="HTTP 请求头中的 X-Forwarded-For | JerryQu 的小站"><meta name=twitter:description content="X-Forwarded-For 是什么？X-Real-Ip 又是什么？它们要怎么用，会不会引入安全隐患？Web 应用中是否可以伪造 IP？这一切的答案尽在本文。"><meta name=twitter:image content=https://st.imququ.com/static/img/blog/ququ_avatar.jpg> <style>@media (pointer:coarse){@media only screen and (max-device-width:1024px){html{font-size:80%!important}}@media only screen and (max-device-width:414px){html{font-size:70%!important}}@media only screen and (max-device-width:320px){html{font-size:90%!important}}}@-webkit-keyframes srFadeInUp{0%{opacity:0;-webkit-transform:translateY(100px);transform:translateY(100px)}to{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes srFadeInUp{0%{opacity:0;-webkit-transform:translateY(100px);transform:translateY(100px)}to{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}}@-webkit-keyframes srFadeInDown{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}to{opacity:0;-webkit-transform:translateY(100px);transform:translateY(100px)}}@keyframes srFadeInDown{0%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}to{opacity:0;-webkit-transform:translateY(100px);transform:translateY(100px)}}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{margin-top:0;padding:0;height:0;min-height:0;opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}}@keyframes fadeOutUp{0%{opacity:1}to{margin-top:0;padding:0;height:0;min-height:0;opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}}</style><link type=image/x-icon rel="shortcut icon" href=""><style>.sf-hidden{display:none!important}</style><link rel=canonical href=https://imququ.com/post/x-forwarded-for-header-in-http.html><body> <div class=container> <div class=left-col> <div class=intrude-less> </div></div> <div class=mid-col> <div class=mid-col-container> <div id=content class=inner><s> </s><script type=application/ld+json>{"@context":"http://schema.org","@type":"BlogPosting","mainEntityOfPage":"https://imququ.com/post/x-forwarded-for-header-in-http.html","headline":"HTTP 请求头中的 X-Forwarded-For","datePublished":"2015-05-02T02:40:45+08:00","dateModified":"2015-05-02T19:41:37+08:00","author":{"@type":"Person","name":"JerryQu"},"publisher":{"@type":"Organization","name":"JerryQu 的小站","logo":{"@type":"ImageObject","url":"https://st.imququ.com/static/img/blog/banner.png","width":600,"height":60}},"image":{"@type":"ImageObject","url":"https://st.imququ.com/static/img/blog/ququ_avatar.jpg","width":1200,"height":1200}}</script> <article class="post post-172"> <div class=meta> <div class=date> <time>May 02, 2015</time></div> <div class="date-modified sf-hidden"> <time>May 02, 2015</time></div> <div class=comment> <a href=#comments>32 Comments</a></div></div> <h1 class=title>HTTP 请求头中的 X-Forwarded-For</h1> <div class=entry-content> <div id=toc-container> <div id=toc class> <p> <strong class>文章目录</strong></p> <ul> <li class> <a href=#toc-0>背景</a> <li> <a href=#toc-1>问题</a> <li> <a href=#toc-2>结论</a> </ul></div></div>  <p class>我一直认为，对于从事 Web 前端开发的同学来说，HTTP 协议以及其他常见的网络知识属于必备项。一方面，前端很多工作如 Web 性能优化，大部分规则都跟 HTTP、HTTPS、SPDY 和 TCP 等协议的特点直接对应，如果不从协议本身出发而是一味地照办教条，很可能适得其反。另一方面，随着 Node.js 的发展壮大，越来越多的前端同学开始写服务端程序，甚至是服务端框架（<a href=http://thinkjs.org/ class>ThinkJS</a> 就是这样由前端工程师开发，并有着众多前端工程师用户的 Node.js 框架），掌握必要的网络知识，对于服务端程序安全、部署、运维等工作来说至关重要。</p>
<p class>我的博客有一个「<a href=https://imququ.com/series.html#toc-7>HTTP 相关</a>」专题，今后会陆续更新更多内容进去，欢迎关注。今天要说的是 HTTP 请求头中的 X-Forwarded-For（XFF）。</p>
<h3 id=toc-0 class>背景</h3>
<p class>通过名字就知道，X-Forwarded-For 是一个 HTTP 扩展头部。HTTP/1.1（RFC 2616）协议并没有对它的定义，它最开始是由 Squid 这个缓存代理软件引入，用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准，被各大 HTTP 代理、负载均衡等转发服务广泛使用，并被写入 <a href=http://tools.ietf.org/html/rfc7239>RFC 7239</a>（Forwarded HTTP Extension）标准之中。</p>
<p class>X-Forwarded-For 请求头格式非常简单，就这样：</p>
<pre><code class=lang-shell>X-Forwarded-For: client, proxy1, proxy2
</code></pre>
<p>可以看到，XFF 的内容由「英文逗号 + 空格」隔开的多个部分组成，最开始的是离服务端最远的设备 IP，然后是每一级代理设备的 IP。</p>
<p class>如果一个 HTTP 请求到达服务器之前，经过了三个代理 Proxy1、Proxy2、Proxy3，IP 分别为 IP1、IP2、IP3，用户真实 IP 为 IP0，那么按照 XFF 标准，服务端最终会收到以下信息：</p>
<pre><code class=lang-shell>X-Forwarded-For: IP0, IP1, IP2
</code></pre>
<p class>Proxy3 直连服务器，它会给 XFF 追加 IP2，表示它是在帮 Proxy2 转发请求。列表中并没有 IP3，IP3 可以在服务端通过 Remote Address 字段获得。我们知道 HTTP 连接基于 TCP 连接，HTTP 协议中没有 IP 的概念，Remote Address 来自 TCP 连接，表示与服务端建立 TCP 连接的设备 IP，在这个例子里就是 IP3。</p>
<p class>Remote Address 无法伪造，因为建立 TCP 连接需要三次握手，如果伪造了源 IP，无法建立 TCP 连接，更不会有后面的 HTTP 请求。不同语言获取 Remote Address 的方式不一样，例如 php 是 <code>$_SERVER["REMOTE_ADDR"]</code>，Node.js 是 <code>req.connection.remoteAddress</code>，但原理都一样。</p>
<h3 id=toc-1>问题</h3>
<p>有了上面的背景知识，开始说问题。我用 Node.js 写了一个最简单的 Web Server 用于测试。HTTP 协议跟语言无关，这里用 Node.js 只是为了方便演示，换成任何其他语言都可以得到相同结论。另外本文用 Nginx 也是一样的道理，如果有兴趣，换成 Apache 或其他 Web Server 也一样。</p>
<p>下面这段代码会监听 <code>9009</code> 端口，并在收到 HTTP 请求后，输出一些信息：</p>
<pre><b class=name>JS</b><code class=lang-js><span class=hljs-keyword>var</span> http = <span class=hljs-built_in>require</span>(<span class=hljs-string>'http'</span>);

http.createServer(<span class=hljs-function><span class=hljs-keyword>function</span> <span class=hljs-params>(req, res)</span> </span>{
    res.writeHead(<span class=hljs-number>200</span>, {<span class=hljs-string>'Content-Type'</span>: <span class=hljs-string>'text/plain'</span>});
    res.write(<span class=hljs-string>'remoteAddress: '</span> + req.connection.remoteAddress + <span class=hljs-string>'\n'</span>);
    res.write(<span class=hljs-string>'x-forwarded-for: '</span> + req.headers[<span class=hljs-string>'x-forwarded-for'</span>] + <span class=hljs-string>'\n'</span>);
    res.write(<span class=hljs-string>'x-real-ip: '</span> + req.headers[<span class=hljs-string>'x-real-ip'</span>] + <span class=hljs-string>'\n'</span>);
    res.end();
}).listen(<span class=hljs-number>9009</span>, <span class=hljs-string>'0.0.0.0'</span>);
</code></pre>
<p>这段代码除了前面介绍过的 Remote Address 和 <code>X-Forwarded-For</code>，还有一个 <code>X-Real-IP</code>，这又是一个自定义头部字段。<code>X-Real-IP</code> 通常被 HTTP 代理用来表示与它产生 TCP 连接的设备 IP，这个设备可能是其他代理，也可能是真正的请求端。需要注意的是，<code>X-Real-IP</code> 目前并不属于任何标准，代理和 Web 应用之间可以约定用任何自定义头来传递这个信息。</p>
<p>现在可以用域名 + 端口号直接访问这个 Node.js 服务，再配一个 Nginx 反向代理：</p>
<pre><b class=name>NGINX</b><code class=lang-nginx><span class=hljs-title>location</span> / {
    <span class=hljs-title>proxy_set_header</span> X-Real-IP <span class=hljs-variable>$remote_addr</span>;
    <span class=hljs-title>proxy_set_header</span> X-Forwarded-For <span class=hljs-variable>$proxy_add_x_forwarded_for</span>;
    <span class=hljs-title>proxy_set_header</span> Host <span class=hljs-variable>$http_host</span>;
    <span class=hljs-title>proxy_set_header</span> X-NginX-Proxy <span class=hljs-built_in>true</span>;

    <span class=hljs-title>proxy_pass</span> <span class=hljs-url>http://127.0.0.1:9009/</span>;
    <span class=hljs-title>proxy_redirect</span> <span class=hljs-built_in>off</span>;
}
</code></pre>
<p>我的 Nginx 监听 <code>80</code> 端口，所以不带端口就可以访问 Nginx 转发过的服务。</p>
<p>测试直接访问 Node 服务：</p>
<pre><b class=name>BASH</b><code class=lang-shell>curl http://t1.imququ.com:<span class=hljs-number>9009</span>/

remoteAddress: <span class=hljs-number>114.248</span>.<span class=hljs-number>238.236</span>
x-forwarded-for: undefined
x-real-ip: undefined
</code></pre>
<p>由于我的电脑直接连接了 Node.js 服务，Remote Address 就是我的 IP。同时我并未指定额外的自定义头，所以后两个字段都是 undefined。</p>
<p>再来访问 Nginx 转发过的服务：</p>
<pre><b class=name>BASH</b><code class=lang-shell>curl http://t1.imququ.com/

remoteAddress: <span class=hljs-number>127.0</span>.<span class=hljs-number>0.1</span>
x-forwarded-for: <span class=hljs-number>114.248</span>.<span class=hljs-number>238.236</span>
x-real-ip: <span class=hljs-number>114.248</span>.<span class=hljs-number>238.236</span>
</code></pre>
<p>这一次，我的电脑是通过 Nginx 访问 Node.js 服务，得到的 Remote Address 实际上是 Nginx 的本地 IP。而前面 Nginx 配置中的这两行起作用了，为请求额外增加了两个自定义头：</p>
<pre><code class=lang-nginx><span class=hljs-title>proxy_set_header</span> X-Real-IP <span class=hljs-variable>$remote_addr</span>;
<span class=hljs-title>proxy_set_header</span> X-Forwarded-For <span class=hljs-variable>$proxy_add_x_forwarded_for</span>;
</code></pre>
<p>实际上，在生产环境中部署 Web 应用，一般都采用上面第二种方式，有很多好处。但这就引入一个隐患：很多 Web 应用为了获取用户真正的 IP，从 HTTP 请求头中获取 IP。</p>
<p>HTTP 请求头可以随意构造，我们通过 curl 的 <code>-H</code> 参数构造 <code>X-Forwarded-For</code> 和 <code>X-Real-IP</code>，再来测试一把。</p>
<p>直接访问 Node.js 服务：</p>
<pre><b class=name>BASH</b><code class=lang-shell>curl http://t1.imququ.com:<span class=hljs-number>9009</span>/ -H <span class=hljs-string>'X-Forwarded-For: 1.1.1.1'</span> -H <span class=hljs-string>'X-Real-IP: 2.2.2.2'</span>

remoteAddress: <span class=hljs-number>114.248</span>.<span class=hljs-number>238.236</span>
x-forwarded-for: <span class=hljs-number>1.1</span>.<span class=hljs-number>1.1</span>
x-real-ip: <span class=hljs-number>2.2</span>.<span class=hljs-number>2.2</span>
</code></pre>
<p>对于 Web 应用来说，<code>X-Forwarded-For</code> 和 <code>X-Real-IP</code> 就是两个普通的请求头，自然就不做任何处理原样输出了。这说明，对于直连部署方式，除了从 TCP 连接中得到的 Remote Address 之外，请求头中携带的 IP 信息都不能信。</p>
<p>访问 Nginx 转发过的服务：</p>
<pre><b class=name>BASH</b><code class=lang-shell>curl http://t1.imququ.com/ -H <span class=hljs-string>'X-Forwarded-For: 1.1.1.1'</span> -H <span class=hljs-string>'X-Real-IP: 2.2.2.2'</span>

remoteAddress: <span class=hljs-number>127.0</span>.<span class=hljs-number>0.1</span>
x-forwarded-for: <span class=hljs-number>1.1</span>.<span class=hljs-number>1.1</span>, <span class=hljs-number>114.248</span>.<span class=hljs-number>238.236</span>
x-real-ip: <span class=hljs-number>114.248</span>.<span class=hljs-number>238.236</span>
</code></pre>
<p>这一次，Nginx 会在 <code>X-Forwarded-For</code> 后追加我的 IP；并用我的 IP 覆盖 <code>X-Real-IP</code> 请求头。这说明，有了 Nginx 的加工，<code>X-Forwarded-For</code> 最后一节以及 <code>X-Real-IP</code> 整个内容无法构造，可以用于获取用户 IP。</p>
<p>用户 IP 往往会被使用与跟 Web 安全有关的场景上，例如检查用户登录地区，基于 IP 做访问频率控制等等。这种场景下，确保 IP 无法构造更重要。经过前面的测试和分析，对于直接面向用户部署的 Web 应用，必须使用从 TCP 连接中得到的 Remote Address；对于部署了 Nginx 这样反向代理的 Web 应用，在正确配置了 Set Header 行为后，可以使用 Nginx 传过来的 <code>X-Real-IP</code> 或 <code>X-Forwarded-For</code> 最后一节（实际上它们一定等价）。</p>
<p>那么，Web 应用自身如何判断请求是直接过来，还是由可控的代理转发来的呢？在代理转发时增加额外的请求头是一个办法，但是不怎么保险，因为请求头太容易构造了。如果一定要这么用，这个自定义头要够长够罕见，还要保管好不能泄露出去。</p>
<p>判断 Remote Address 是不是本地 IP 也是一种办法，不过也不完善，因为在 Nginx 所处服务器上访问，无论直连还是走 Nginx 代理，Remote Address 都是 127.0.0.1。这个问题还好通常可以忽略，更麻烦的是，反向代理服务器和实际的 Web 应用不一定部署在同一台服务器上。所以更合理的做法是收集所有代理服务器 IP 列表，Web 应用拿到 Remote Address 后逐一比对来判断是以何种方式访问。</p>
<p>通常，为了简化逻辑，生产环境会封掉通过带端口直接访问 Web 应用的形式，只允许通过 Nginx 来访问。那是不是这样就没问题了呢？也不见得。</p>
<p>首先，如果用户真的是通过代理访问 Nginx，<code>X-Forwarded-For</code> 最后一节以及 <code>X-Real-IP</code> 得到的是代理的 IP，安全相关的场景只能用这个，但有些场景如根据 IP 显示所在地天气，就需要尽可能获得用户真实 IP，这时候 <code>X-Forwarded-For</code> 中第一个 IP 就可以排上用场了。这时候需要注意一个问题，还是拿之前的例子做测试：</p>
<pre><b class=name>BASH</b><code class=lang-shell>curl http://t1.imququ.com/ -H <span class=hljs-string>'X-Forwarded-For: unknown, &lt;&gt;"1.1.1.1'</span>

remoteAddress: <span class=hljs-number>127.0</span>.<span class=hljs-number>0.1</span>
x-forwarded-for: unknown, &lt;&gt;<span class=hljs-string>"1.1.1.1, 114.248.238.236
x-real-ip: 114.248.238.236</span>
</code></pre>
<p><code>X-Forwarded-For</code> 最后一节是 Nginx 追加上去的，但之前部分都来自于 Nginx 收到的请求头，这部分用户输入内容完全不可信。使用时需要格外小心，符合 IP 格式才能使用，不然容易引发 SQL 注入或 XSS 等安全漏洞。</p>
<h3 id=toc-2>结论</h3>
<ol>
<li>直接对外提供服务的 Web 应用，在进行与安全有关的操作时，只能通过 Remote Address 获取 IP，不能相信任何请求头；</li>
<li>使用 Nginx 等 Web Server 进行反向代理的 Web 应用，在配置正确的前提下，要用 <code>X-Forwarded-For</code> 最后一节 或 <code>X-Real-IP</code> 来获取 IP（因为 Remote Address 得到的是 Nginx 所在服务器的内网 IP）；同时还应该禁止 Web 应用直接对外提供服务；</li>
<li>在与安全无关的场景，例如通过 IP 显示所在地天气，可以从 <code>X-Forwarded-For</code> 靠前的位置获取 IP，但是需要校验 IP 格式合法性；</li>
</ol>
<p>PS：网上有些文章建议这样配置 Nginx，其实并不合理：</p>
<pre class><code class=lang-nginx><span class=hljs-title>proxy_set_header</span> X-Real-IP <span class=hljs-variable>$remote_addr</span>;
<span class=hljs-title>proxy_set_header</span> X-Forwarded-For <span class=hljs-variable>$remote_addr</span>;
</code></pre>
<p class>这样配置之后，安全性确实提高了，但是也导致请求到达 Nginx 之前的所有代理信息都被抹掉，无法为真正使用代理的用户提供更好的服务。还是应该弄明白这中间的原理，具体场景具体分析。</p>
 <p class> 本文链接：<a rel=bookmark title="Permalink to HTTP 请求头中的 X-Forwarded-For" href=https://imququ.com/post/x-forwarded-for-header-in-http.html>https://imququ.com/post/x-forwarded-for-header-in-http.html</a>，<a href=#comments>参与评论 »</a></p> <p class>--<acronym title="End of File" class>EOF</acronym>--</p> <p class=post-info> 发表于&nbsp;<span class=date>2015-05-02 02:40:45</span> ，并被添加「 <a href="https://imququ.com/search.html?s=tag%3AHTTP">HTTP</a> 、 <a href="https://imququ.com/search.html?s=tag%3AProxy">Proxy</a> 」标签 ，最后修改于&nbsp;<span class=date>2015-05-02 19:41:37</span> 。<a href=https://imququ.com/post/x-forwarded-for-header-in-http.md>查看本文 Markdown 版本 »</a></p> <p class=copyright-info>本站使用「<a href=http://creativecommons.org/licenses/by/4.0/deed.zh class>署名 4.0 国际</a>」创作共享协议，<a href=https://imququ.com/post/about.html#toc-1 class>相关说明 »</a></p>   </div></article>   </div></div> </div></div> <gr-manager></gr-manager><script data-template-shadow-root>(() => { document.currentScript.remove(); const processNode = node => { node.querySelectorAll("template[shadowmode]").forEach(element=>{ if (!element.parentElement.shadowRoot) { const shadowRoot = element.parentElement.attachShadow({mode:element.getAttribute("shadowmode"),delegatesFocus:Boolean(element.getAttribute("delegatesfocus"))}); shadowRoot.innerHTML = element.innerHTML; element.remove(); processNode(shadowRoot); } }) }; const FORBIDDEN_TAG_NAMES = ["a","area","audio","base","br","col","command","embed","hr","img","iframe","input","keygen","link","meta","param","source","track","video","wbr"]; const NOTE_TAGNAME = "single-file-note"; const NOTE_CLASS = "note"; const NOTE_ANCHORED_CLASS = "note-anchored"; const NOTE_SELECTED_CLASS = "note-selected"; const NOTE_MOVING_CLASS = "note-moving"; const NOTE_MASK_MOVING_CLASS = "note-mask-moving"; const MASK_CLASS = "single-file-mask"; const HIGHLIGHT_CLASS = "single-file-highlight"; const NOTES_WEB_STYLESHEET = ".note { all: initial; display: flex; flex-direction: column; height: 150px; width: 150px; position: absolute; top: 10px; left: 10px; border: 1px solid rgb(191, 191, 191); z-index: 2147483646; box-shadow: 2px 2px 5px rgba(33, 33, 33, .7); min-height: 100px; min-width: 100px; } .note-selected { z-index: 2147483647; } .note-hidden { display: none; } .note-collapsed { min-height: 30px; max-height: 30px; overflow: hidden; } .note textarea { all: initial; white-space: pre; font-family: Arial, Helvetica, sans-serif; font-size: 14px; padding: 3px; height: 100%; border: 1px solid transparent; resize: none; } .note textarea:focus { border: 1px dotted rgb(160, 160, 160); } .note header { all: initial; min-height: 30px; cursor: grab; user-select: none; } .note .note-remove { all: initial; position: absolute; right: 0px; top: 2px; padding: 5px; opacity: .5; cursor: pointer; user-select: none; } .note .note-anchor { all: initial; position: absolute; left: 0px; top: 2px; padding: 5px; opacity: .25; cursor: pointer; } .note .note-resize { all: initial; position: absolute; bottom: -5px; right: -5px; height: 15px; width: 15px; cursor: nwse-resize; user-select: none; } .note .note-remove:hover { opacity: 1; } .note .note-anchor:hover { opacity: .5; } .note-anchored .note-anchor { opacity: .5; } .note-anchored .note-anchor:hover { opacity: 1; } .note-moving { opacity: .5; } .note-moving * { cursor: grabbing; } .note-yellow header { background-color: #f5f545; } .note-yellow textarea { background-color: #ffff7c; } .note-pink header { background-color: #ffa59f; } .note-pink textarea { background-color: #ffbbb6; } .note-blue header { background-color: #84c8ff; } .note-blue textarea { background-color: #95d0ff; } .note-green header { background-color: #93ef8d; } .note-green textarea { background-color: #9cff95; }"; const MASK_WEB_STYLESHEET = ".note-mask { all: initial; position: fixed; z-index: 2147483645; pointer-events: none; } .note-mask-moving { background-color: rgba(236, 234, 234, 0.5); } .page-mask { all: initial; position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483646; } .page-mask-active { width: 100vw; height: 100vh; }"; const NOTE_HEADER_HEIGHT = 25; const PAGE_MASK_ACTIVE_CLASS = "page-mask-active"; const REMOVED_CONTENT_CLASS = "single-file-removed"; const reflowNotes = function reflowNotes() { document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => { const noteElement = containerElement.shadowRoot.querySelector("." + NOTE_CLASS); const noteBoundingRect = noteElement.getBoundingClientRect(); const anchorElement = getAnchorElement(containerElement); const anchorBoundingRect = anchorElement.getBoundingClientRect(); const maxX = anchorBoundingRect.x + Math.max(0, anchorBoundingRect.width - noteBoundingRect.width); const minX = anchorBoundingRect.x; const maxY = anchorBoundingRect.y + Math.max(0, anchorBoundingRect.height - NOTE_HEADER_HEIGHT); const minY = anchorBoundingRect.y; let left = parseInt(noteElement.style.getPropertyValue("left")); let top = parseInt(noteElement.style.getPropertyValue("top")); if (noteBoundingRect.x > maxX) { left -= noteBoundingRect.x - maxX; } if (noteBoundingRect.x < minX) { left += minX - noteBoundingRect.x; } if (noteBoundingRect.y > maxY) { top -= noteBoundingRect.y - maxY; } if (noteBoundingRect.y < minY) { top += minY - noteBoundingRect.y; } noteElement.style.setProperty("position", "absolute"); noteElement.style.setProperty("left", left + "px"); noteElement.style.setProperty("top", top + "px"); }); }; const addNoteRef = function addNoteRef(anchorElement, noteId) { const noteRefs = getNoteRefs(anchorElement); noteRefs.push(noteId); setNoteRefs(anchorElement, noteRefs); }; const deleteNoteRef = function deleteNoteRef(containerElement, noteId) { const anchorElement = getAnchorElement(containerElement); const noteRefs = getNoteRefs(anchorElement).filter(noteRefs => noteRefs != noteId); if (noteRefs.length) { setNoteRefs(anchorElement, noteRefs); } else { delete anchorElement.dataset.singleFileNoteRefs; } }; const getNoteRefs = function getNoteRefs(anchorElement) { return JSON.parse("[" + (anchorElement.dataset.singleFileNoteRefs || "") + "]"); }; const setNoteRefs = function setNoteRefs(anchorElement, noteRefs) { anchorElement.dataset.singleFileNoteRefs = noteRefs.toString(); }; const getAnchorElement = function getAnchorElement(containerElement) { return document.querySelector("[data-single-file-note-refs^=" + JSON.stringify(containerElement.dataset.noteId) + "], [data-single-file-note-refs$=" + JSON.stringify(containerElement.dataset.noteId) + "], [data-single-file-note-refs*=" + JSON.stringify("," + containerElement.dataset.noteId + ",") + "]"); }; const getMaskElement = function getMaskElement(className, containerClassName) { let maskElement = document.documentElement.querySelector("." + className); if (!maskElement) { maskElement = document.createElement("div"); const maskContainerElement = document.createElement("div"); if (containerClassName) { maskContainerElement.classList.add(containerClassName); } maskContainerElement.classList.add(MASK_CLASS); const firstNote = document.querySelector(NOTE_TAGNAME); if (firstNote && firstNote.parentElement == document.documentElement) { document.documentElement.insertBefore(maskContainerElement, firstNote); } else { document.documentElement.appendChild(maskContainerElement); } maskElement.classList.add(className); const maskShadow = maskContainerElement.attachShadow({ mode: "open" }); maskShadow.appendChild(getStyleElement(MASK_WEB_STYLESHEET)); maskShadow.appendChild(maskElement); return maskElement; } }; const getStyleElement = function getStyleElement(stylesheet) { const linkElement = document.createElement("style"); linkElement.textContent = stylesheet; return linkElement; }; const attachNoteListeners = function attachNoteListeners(containerElement, editable = false) { const SELECT_PX_THRESHOLD = 4; const COLLAPSING_NOTE_DELAY = 750; const noteShadow = containerElement.shadowRoot; const noteElement = noteShadow.childNodes[1]; const headerElement = noteShadow.querySelector("header"); const mainElement = noteShadow.querySelector("textarea"); const noteId = containerElement.dataset.noteId; const resizeElement = noteShadow.querySelector(".note-resize"); const anchorIconElement = noteShadow.querySelector(".note-anchor"); const removeNoteElement = noteShadow.querySelector(".note-remove"); mainElement.readOnly = !editable; if (!editable) { anchorIconElement.style.setProperty("display", "none", "important"); } else { anchorIconElement.style.removeProperty("display"); } headerElement.ontouchstart = headerElement.onmousedown = event => { if (event.target == headerElement) { collapseNoteTimeout = setTimeout(() => noteElement.classList.toggle("note-collapsed"), COLLAPSING_NOTE_DELAY); event.preventDefault(); const position = getPosition(event); const clientX = position.clientX; const clientY = position.clientY; const boundingRect = noteElement.getBoundingClientRect(); const deltaX = clientX - boundingRect.left; const deltaY = clientY - boundingRect.top; maskPageElement.classList.add(PAGE_MASK_ACTIVE_CLASS); document.documentElement.style.setProperty("user-select", "none", "important"); anchorElement = getAnchorElement(containerElement); displayMaskNote(); selectNote(noteElement); moveNote(event, deltaX, deltaY); movingNoteMode = { event, deltaX, deltaY }; document.documentElement.ontouchmove = document.documentElement.onmousemove = event => { clearTimeout(collapseNoteTimeout); if (!movingNoteMode) { movingNoteMode = { deltaX, deltaY }; } movingNoteMode.event = event; moveNote(event, deltaX, deltaY); }; } }; resizeElement.ontouchstart = resizeElement.onmousedown = event => { event.preventDefault(); resizingNoteMode = true; selectNote(noteElement); maskPageElement.classList.add(PAGE_MASK_ACTIVE_CLASS); document.documentElement.style.setProperty("user-select", "none", "important"); document.documentElement.ontouchmove = document.documentElement.onmousemove = event => { event.preventDefault(); const { clientX, clientY } = getPosition(event); const boundingRectNote = noteElement.getBoundingClientRect(); noteElement.style.width = clientX - boundingRectNote.left + "px"; noteElement.style.height = clientY - boundingRectNote.top + "px"; }; }; anchorIconElement.ontouchend = anchorIconElement.onclick = event => { event.preventDefault(); noteElement.classList.toggle(NOTE_ANCHORED_CLASS); if (!noteElement.classList.contains(NOTE_ANCHORED_CLASS)) { deleteNoteRef(containerElement, noteId); addNoteRef(document.documentElement, noteId); } }; removeNoteElement.ontouchend = removeNoteElement.onclick = event => { event.preventDefault(); deleteNoteRef(containerElement, noteId); containerElement.remove(); }; noteElement.onmousedown = () => { selectNote(noteElement); }; function moveNote(event, deltaX, deltaY) { event.preventDefault(); const { clientX, clientY } = getPosition(event); noteElement.classList.add(NOTE_MOVING_CLASS); if (editable) { if (noteElement.classList.contains(NOTE_ANCHORED_CLASS)) { deleteNoteRef(containerElement, noteId); anchorElement = getTarget(clientX, clientY) || document.documentElement; addNoteRef(anchorElement, noteId); } else { anchorElement = document.documentElement; } } document.documentElement.insertBefore(containerElement, maskPageElement.getRootNode().host); noteElement.style.setProperty("left", (clientX - deltaX) + "px"); noteElement.style.setProperty("top", (clientY - deltaY) + "px"); noteElement.style.setProperty("position", "fixed"); displayMaskNote(); } function displayMaskNote() { if (anchorElement == document.documentElement || anchorElement == document.documentElement) { maskNoteElement.classList.remove(NOTE_MASK_MOVING_CLASS); } else { const boundingRectAnchor = anchorElement.getBoundingClientRect(); maskNoteElement.classList.add(NOTE_MASK_MOVING_CLASS); maskNoteElement.style.setProperty("top", boundingRectAnchor.y + "px"); maskNoteElement.style.setProperty("left", boundingRectAnchor.x + "px"); maskNoteElement.style.setProperty("width", boundingRectAnchor.width + "px"); maskNoteElement.style.setProperty("height", boundingRectAnchor.height + "px"); } } function selectNote(noteElement) { if (selectedNote) { selectedNote.classList.remove(NOTE_SELECTED_CLASS); } noteElement.classList.add(NOTE_SELECTED_CLASS); selectedNote = noteElement; } function getTarget(clientX, clientY) { const targets = Array.from(document.elementsFromPoint(clientX, clientY)).filter(element => element.tagName.toLowerCase() != NOTE_TAGNAME && !element.classList.contains(MASK_CLASS)); if (!targets.includes(document.documentElement)) { targets.push(document.documentElement); } let newTarget, target = targets[0], boundingRect = target.getBoundingClientRect(); newTarget = determineTargetElement("floor", target, clientX - boundingRect.left, getMatchedParents(target, "left")); if (newTarget == target) { newTarget = determineTargetElement("ceil", target, boundingRect.left + boundingRect.width - clientX, getMatchedParents(target, "right")); } if (newTarget == target) { newTarget = determineTargetElement("floor", target, clientY - boundingRect.top, getMatchedParents(target, "top")); } if (newTarget == target) { newTarget = determineTargetElement("ceil", target, boundingRect.top + boundingRect.height - clientY, getMatchedParents(target, "bottom")); } target = newTarget; while (boundingRect = target && target.getBoundingClientRect(), boundingRect && boundingRect.width <= SELECT_PX_THRESHOLD && boundingRect.height <= SELECT_PX_THRESHOLD) { target = target.parentElement; } return target; } function getMatchedParents(target, property) { let element = target, matchedParent, parents = []; do { const boundingRect = element.getBoundingClientRect(); if (element.parentElement && !element.parentElement.tagName.toLowerCase() != NOTE_TAGNAME && !element.classList.contains(MASK_CLASS)) { const parentBoundingRect = element.parentElement.getBoundingClientRect(); matchedParent = Math.abs(parentBoundingRect[property] - boundingRect[property]) <= SELECT_PX_THRESHOLD; if (matchedParent) { if (element.parentElement.clientWidth > SELECT_PX_THRESHOLD && element.parentElement.clientHeight > SELECT_PX_THRESHOLD && ((element.parentElement.clientWidth - element.clientWidth > SELECT_PX_THRESHOLD) || (element.parentElement.clientHeight - element.clientHeight > SELECT_PX_THRESHOLD))) { parents.push(element.parentElement); } element = element.parentElement; } } else { matchedParent = false; } } while (matchedParent && element); return parents; } function determineTargetElement(roundingMethod, target, widthDistance, parents) { if (Math[roundingMethod](widthDistance / SELECT_PX_THRESHOLD) <= parents.length) { target = parents[parents.length - Math[roundingMethod](widthDistance / SELECT_PX_THRESHOLD) - 1]; } return target; } }; const anchorNote = function anchorNote(event, noteElement, deltaX, deltaY) { event.preventDefault(); const { clientX, clientY } = getPosition(event); document.documentElement.style.removeProperty("user-select"); noteElement.classList.remove(NOTE_MOVING_CLASS); maskNoteElement.classList.remove(NOTE_MASK_MOVING_CLASS); maskPageElement.classList.remove(PAGE_MASK_ACTIVE_CLASS); const headerElement = noteElement.querySelector("header"); headerElement.ontouchmove = document.documentElement.onmousemove = null; let currentElement = anchorElement; let positionedElement; while (currentElement.parentElement && !positionedElement) { if (!FORBIDDEN_TAG_NAMES.includes(currentElement.tagName.toLowerCase())) { const currentElementStyle = getComputedStyle(currentElement); if (currentElementStyle.position != "static") { positionedElement = currentElement; } } currentElement = currentElement.parentElement; } if (!positionedElement) { positionedElement = document.documentElement; } const containerElement = noteElement.getRootNode().host; if (positionedElement == document.documentElement) { const firstMaskElement = document.querySelector("." + MASK_CLASS); document.documentElement.insertBefore(containerElement, firstMaskElement); } else { positionedElement.appendChild(containerElement); } const boundingRectPositionedElement = positionedElement.getBoundingClientRect(); const stylePositionedElement = window.getComputedStyle(positionedElement); const borderX = parseInt(stylePositionedElement.getPropertyValue("border-left-width")); const borderY = parseInt(stylePositionedElement.getPropertyValue("border-top-width")); noteElement.style.setProperty("position", "absolute"); noteElement.style.setProperty("left", (clientX - boundingRectPositionedElement.x - deltaX - borderX) + "px"); noteElement.style.setProperty("top", (clientY - boundingRectPositionedElement.y - deltaY - borderY) + "px"); }; const getPosition = function getPosition(event) { if (event.touches && event.touches.length) { const touch = event.touches[0]; return touch; } else { return event; } }; const onMouseUp = function onMouseUp(event) { if (highlightSelectionMode) { highlightSelection(); } if (removeHighlightMode) { let element = event.target, done; while (element && !done) { if (element.classList.contains(HIGHLIGHT_CLASS)) { document.querySelectorAll("." + HIGHLIGHT_CLASS + "[data-singlefile-highlight-id=" + JSON.stringify(element.dataset.singlefileHighlightId) + "]").forEach(highlightedElement => { resetHighlightedElement(highlightedElement); }); done = true; } element = element.parentElement; } } if (resizingNoteMode) { resizingNoteMode = false; document.documentElement.style.removeProperty("user-select"); maskPageElement.classList.remove(PAGE_MASK_ACTIVE_CLASS); document.documentElement.ontouchmove = document.documentElement.onmousemove = null; } if (movingNoteMode) { anchorNote(movingNoteMode.event || event, selectedNote, movingNoteMode.deltaX, movingNoteMode.deltaY); movingNoteMode = null; document.documentElement.ontouchmove = document.documentElement.onmousemove = null; } if (collapseNoteTimeout) { clearTimeout(collapseNoteTimeout); collapseNoteTimeout = null; } if (cuttingMode) { let element = event.target; if (document.documentElement != element && element.tagName.toLowerCase() != NOTE_TAGNAME) { element.classList.add(REMOVED_CONTENT_CLASS); removedElements.push(element); } } }; const maskNoteElement = getMaskElement("note-mask"); const maskPageElement = getMaskElement("page-mask", "single-file-page-mask"); let selectedNote, highlightSelectionMode, removeHighlightMode, resizingNoteMode, movingNoteMode, collapseNoteTimeout, cuttingMode; window.onresize = reflowNotes; document.documentElement.onmouseup = document.documentElement.ontouchend = onMouseUp; window.addEventListener("DOMContentLoaded", () => { processNode(document); reflowNotes(); document.querySelectorAll("single-file-note").forEach(noteElement => attachNoteListeners(noteElement)); }); })()</script><style>.single-file-highlight-yellow { background-color: #ffff7c !important; color: black !important; } span.single-file-highlight-yellow { display: inline !important; } .single-file-highlight-pink { background-color: #ffbbb6 !important; color: black !important; } span.single-file-highlight-pink { display: inline !important; } .single-file-highlight-blue { background-color: #95d0ff !important; color: black !important; } span.single-file-highlight-blue { display: inline !important; } .single-file-highlight-green { background-color: #93ef8d !important; color: black !important; } span.single-file-highlight-green { display: inline !important; } .single-file-highlight-hidden { background-color: inherit !important; color: inherit !important; } .single-file-mask { all: initial; display: contents !important; } .single-file-mask.single-file-page-mask { opacity: .99 !important; } single-file-note { all: initial !important; display: contents !important; } .single-file-hover { outline: 1px solid red !important; } .single-file-removed { display: none !important; float: none !important; position: static !important; visibility: collapse !important; }</style>